# Importación de todas las librerías usadas durante el informe y breve descripcion
library(readr) # Libreria para poder importar los datos desde un csv.
library(ggplot2) # Libreria para poder hacer gráficos.
library(DT) # Libreria para poder visualizar dataframes en qmd de manera interactiva.
library(hms) # Libreria para poder tratar datos referentes a horas, minutos y segundos.
library(dplyr) # Libreria para poder manipular dataframes.
library(tidyr) # Libreria para poder transformar dataframes.
library(e1071) # para skewness() y kurtosis()
library(stringr) # Para la limpieza de nombres del Dataframe.
library(knitr) # Para tablas bien formateadas
library(viridis) # Paletas de color para los gráficos.
library(plotly) # Para gráficas de mapas.
library(gapminder) # Usaremos este paquete para la lista de países estándar
library(patchwork) # para combinar gráficos ggplot2
library(skimr)
library(naniar)
library(grid) # para funciones de gráficos adicionales
library(kableExtra)1 Importación de librerías.
2 Introducción
El maratón de Tokyo 2025 se celebró, en su 18ª edición, el domingo 2 de marzo de 2025. Esta edición forma parte de los World Marathon Majors y abrió la temporada 2025 de los grandes maratones internacionales. El recorrido atraviesa distintos puntos icónicos de la ciudad de Tokyo, estando su inicio frente al edificio del Gobierno Metropolitano y la línea de meta cerca de la estación Tokyo/Gyoko-dori Avenue. El maratón de Tokyo es un evento de gran participación, tanto de atletas de élite como de aficionados.
En el presente documento se realizará un análisis exploratorio de datos (EDA) con el objetivo de extraer información relevante acerca de los resultados de los corredores que participaron en dicha prueba.
3 Presentación y Descripción del Dataset
Los resultados del maratón de Tokyo han sido extraídos a través del siguiente enlace. La extracción se ha realizado a través de técnicas de Web Scrapping con el objetivo de poder obtener datos relevantes referentes a los resultados de los corredores participantes.
3.1 Importación del Dataset
A continuación, se realiza la correspondiente importación de los datos extraídos:
resultadosTokyo2025 <- read_csv(
"data/Maraton_Tokyo/marathon_tokyo_results_2025.csv",
col_types = cols(
BIB = col_integer(),
Nombre = col_character(),
Nacionalidad = col_character(),
Genero = col_character(),
Edad = col_integer(),
tiempo_oficial = col_time(format = "%H:%M:%S"),
parcial_5km = col_time(format = "%H:%M:%S"),
parcial_10km = col_time(format = "%H:%M:%S"),
parcial_15km = col_time(format = "%H:%M:%S"),
parcial_20km = col_time(format = "%H:%M:%S"),
medio_maraton = col_time(format = "%H:%M:%S"),
parcial_25km = col_time(format = "%H:%M:%S"),
parcial_30km = col_time(format = "%H:%M:%S"),
parcial_35km = col_time(format = "%H:%M:%S"),
parcial_40km = col_time(format = "%H:%M:%S")
),
quote = "\""
)
# Transformacion a formato dataframe.
resultadosTokyo2025 <- as.data.frame(resultadosTokyo2025)3.2 Descripción del Dataset
El dataframe importado, resultadosTokyo2025, consta de las siguientes variables:
| Variable | Tipo | Descripción | Unidades |
|---|---|---|---|
| BIB | Numérica/Entero | Número de dorsal asignado al corredor, valor único | Número entero |
| Nombre | Cadena de texto | Nombre y apellidos del corredor | Texto |
| Nacionalidad | Cadena de texto | País de procedencia del corredor | Texto |
| Genero | Categórica | Género del corredor | Texto |
| Edad | Numérica/Entero | Edad del corredor | Años |
| tiempo_oficial | Tiempo | Tiempo total oficial de la maratón (gross time) | hh:mm:ss |
| parcial_5km | Tiempo | Tiempo de paso en el km 5 | hh:mm:ss |
| parcial_10km | Tiempo | Tiempo de paso en el km 10 | hh:mm:ss |
| parcial_15km | Tiempo | Tiempo de paso en el km 15 | hh:mm:ss |
| parcial_20km | Tiempo | Tiempo de paso en el km 20 | hh:mm:ss |
| medio_maraton | Tiempo | Tiempo al paso del medio maratón (21,097 km) | hh:mm:ss |
| parcial_25km | Tiempo | Tiempo de paso en el km 25 | hh:mm:ss |
| parcial_30km | Tiempo | Tiempo de paso en el km 30 | hh:mm:ss |
| parcial_35km | Tiempo | Tiempo de paso en el km 35 | hh:mm:ss |
| parcial_40km | Tiempo | Tiempo de paso en el km 40 | hh:mm:ss |
Nota: El gross time es el tiempo que tarda un corredor en terminar la maratón desde que se da el pistoletazo de salida, no desde que cruza la línea de inicio de la prueba
3.3 Lectura de una fila
Una vez que conocemos el significado de cada variable por separado, se va a proceder a la lectura de la primera fila del dataframe con el objetivo de mejorar la comprensión sobre el formato de los datos:
# Lo visualizamos con la libreria DT porque es más interactiva a la hora de generar el documento qmd.
datatable(
resultadosTokyo2025,
options = list(
pageLength = 1, # cuántas filas mostrar
scrollX = TRUE, # habilita scroll horizontal si la fila es muy ancha
dom = 't' # solo muestra la tabla sin paginación ni búsqueda
),
rownames = FALSE # quitar número de fila en la tabla
)Se puede observar al corredor Tadese Takele, de nacionalidad Etíope, que corrió la maratón con el dorsal número 5. Tadese completó la maratón a sus 22 años con un tiempo de 2 horas, 3 minutos y 23 segundos, pudiendose observar sus tiempos de paso cada 5 kilómetros y en el punto de la media maratón.
3.4 Dimensiones del dataset
filas <- nrow(resultadosTokyo2025)
columnas <- ncol(resultadosTokyo2025)
cat(
"El dataframe resultadosTokyo2025 contiene",
filas,
"filas y",
columnas,
"columnas."
)El dataframe resultadosTokyo2025 contiene 36173 filas y 15 columnas.
# Elimino el número de filas y columnas con el objetivo de no sobrecargar el environment.
rm(filas, columnas)4 Preparación y limpieza de los datos
Como se ha podido observar en la sección Sección 3.3, el dataset presenta diversas particularidades que requieren atención antes de proceder con el análisis estadístico univariante. En primer lugar, algunas variables presentan formatos mixtos (japonés e inglés separados por “/”), lo cual dificulta la legibilidad y el procesamiento posterior. En segundo lugar, los tiempos están almacenados en formato HMS (horas:minutos:segundos), lo que complica operaciones aritméticas y comparaciones.
Esta sección se procederá a la preparación del dataset mediante:
Detección y tratamiento de valores nulos e inconsistencias: identificación de datos faltantes, duplicados y valores atípicos que comprometan la calidad del análisis.
Estandarización de formatos: homogeneización de variables textuales (nombres, nacionalidades) para mejorar la legibilidad, facilitar una posible portabilidad del código (permitiendo cruzar datos con otros maratones) y habilitar nuevas formas de análisis posteriores.
Transformación y creación de variables derivadas: conversión de tiempos a formato numérico (segundos), cálculo de ritmos parciales cada 5 km, y categorización de corredores según nivel de rendimiento (élite, semi-profesional, amateur).
Estas transformaciones permitirán un análisis univariante y multivariante más robusto y facilitarán la interpretación de patrones de rendimiento en la carrera.
4.1 Detección de Problemas de Calidad
En la actual sección se procederá a realizar un análisis de los posibles valores tomados por cada variable del conjunto de datos con el objetivo de detectar posibles irregularidades en los datos, ya sea a través de datos nulos (ya detectados u ocultos), duplicados, inconsistencias o valores atípicos. Para ello, vamos a comenzar con un vistazo a los datos agrupado por cada variable:
# Resumen global de todas las variables.
skimr::skim(resultadosTokyo2025)| Name | resultadosTokyo2025 |
| Number of rows | 36173 |
| Number of columns | 15 |
| _______________________ | |
| Column type frequency: | |
| character | 3 |
| difftime | 10 |
| numeric | 2 |
| ________________________ | |
| Group variables | None |
Variable type: character
| skim_variable | n_missing | complete_rate | min | max | empty | n_unique | whitespace |
|---|---|---|---|---|---|---|---|
| Nombre | 0 | 1 | 11 | 81 | 0 | 35916 | 0 |
| Nacionalidad | 0 | 1 | 1 | 24 | 0 | 127 | 0 |
| Genero | 0 | 1 | 6 | 19 | 0 | 3 | 0 |
Variable type: difftime
| skim_variable | n_missing | complete_rate | min | max | median | n_unique |
|---|---|---|---|---|---|---|
| tiempo_oficial | 0 | 1 | 7403 secs | 25218 secs | 04:36:13.0 | 13775 |
| parcial_5km | 10 | 1 | 865 secs | 3855 secs | 00:28:27.0 | 1583 |
| parcial_10km | 7 | 1 | 1735 secs | 6491 secs | 00:56:39.0 | 2980 |
| parcial_15km | 9 | 1 | 2610 secs | 8654 secs | 01:25:07.0 | 4407 |
| parcial_20km | 9 | 1 | 3487 secs | 11581 secs | 01:54:24.0 | 5954 |
| medio_maraton | 4 | 1 | 3678 secs | 12197 secs | 02:00:56.0 | 6299 |
| parcial_25km | 14 | 1 | 4363 secs | 14527 secs | 02:24:24.0 | 7523 |
| parcial_30km | 8 | 1 | 5242 secs | 17334 secs | 02:56:54.0 | 9152 |
| parcial_35km | 5 | 1 | 6133 secs | 20184 secs | 03:31:24.0 | 10617 |
| parcial_40km | 11 | 1 | 7019 secs | 23424 secs | 04:08:14.5 | 12102 |
Variable type: numeric
| skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
|---|---|---|---|---|---|---|---|---|---|---|
| BIB | 0 | 1 | 29348.23 | 11716.42 | 1 | 19748 | 29417 | 39083 | 91050 | ▃▇▆▁▁ |
| Edad | 0 | 1 | 46.59 | 11.07 | 19 | 38 | 47 | 55 | 84 | ▂▆▇▃▁ |
# Resumen específico: Variable BIB.
cat("La variable BIB contiene", n_distinct(resultadosTokyo2025$BIB), "valores distintos")La variable BIB contiene 36173 valores distintos
# Resumen específico: Variable EDAD.
cat("La variable Edad tiene los siguientes valores distintos: ", unique(resultadosTokyo2025$Genero))La variable Edad tiene los siguientes valores distintos: 男性/Men 女性/Women ノンバイナリー/Non-binary
# Resumen específico: Variable Nacionalidad.
cat("La variable nacionalidad toma los siguientes valores: ", unique(resultadosTokyo2025$Nacionalidad))La variable nacionalidad toma los siguientes valores: ETHIOPIA KENYA SWEDEN UGANDA 日本 PR OF CHINA GERMANY GREAT BRITAIN & N.I. UNITED STATES FRANCE BAHRAIN CHINESE TAIPEI HONG KONG, CHINA AUSTRALIA PHILIPPINES POLAND MONGOLIA UKRAINE ESTONIA BELGIUM MALAYSIA CAMBODIA SPAIN VENEZUELA VIETNAM UZBEKISTAN SWITZERLAND SINGAPORE COSTA RICA RUSSIA ITALY BRAZIL NEPAL NEW ZEALAND KOREA PORTUGAL CANADA CHILE ARGENTINA THAILAND SOUTH AFRICA NETHERLANDS MEXICO HONDURAS COLOMBIA NORWAY LITHUANIA CZECH REPUBLIC ISRAEL DOMINICAN REPUBLIC FINLAND GUATEMALA GREECE KAZAKHSTAN PERU MACAO HUNGARY DENMARK CROATIA IRELAND INDONESIA - ECUADOR AUSTRIA ROMANIA PUERTO RICO UNITED ARAB EMIRATES PANAMA BURUNDI SLOVENIA EGYPT BARBADOS BULGARIA BRUNEI BERMUDA NICARAGUA BELARUS DPR OF KOREA MOLDOVA INDIA EL SALVADOR LATVIA SERBIA PALESTINE F Y REP. OF MACEDONIA VIRGIN ISLANDS TURKEY SLOVAK REPUBLIC URUGUAY ICELAND AFGHANISTAN ALGERIA NIGERIA PAKISTAN MOROCCO PARAGUAY TANZANIA MALDIVES ARMENIA SAINT KITTS AND NEVIS TUNISIA JORDAN CYPRUS BAHAMAS CAYMAN ISLANDS ZIMBABWE GUAM MALTA LUXEMBOURG GUYANA MAURITIUS SAUDI ARABIA BHUTAN LIBYA SUDAN REPUBLIC Of YEMEN BOLIVIA QATAR BENIN BELIZE ZAMBIA JAMAICA ISLAMIC REPUBLIC OF IRAN TURKMENISTAN SAINT LUCIA MYANMAR LAOS
De este resumen podemos resaltar que:
- Existen 35916 nombres únicos en el conjunto de datos, pero 36173 corredores, habrá que analizar posibles duplicados.
- En los parciales, existen datos faltantes NA.
- En la variable Nacionalidad, existen valores de tipo - que determinan datos faltantes ocultos.
Tras este primer análisis global de las variables, vamos a proceder a identificar concretamente los problemas de calidad y tras ello, a tratarlos.
4.1.1 Duplicados e Inconsistencias
Comenzamos realizando un análisis de posibles datos duplicados. Se ha podido observar en el análisis inicial que teníamos datos duplicados en las variables:
- Nombre
- Parciales.
Cabe destacar que es normal que, al haber tantos corredores, existan datos duplicados en los parciales, ya que los corredores podían encontrarse en paralelo (o con diferencia menor a un minuto) al paso de cada sector y por ello tienen en esa variable el mismo dato.
Respecto a los duplicados en la variable Nombre, vamos a proceder a hacer un pequeño análisis con el objetivo de identificar el por qué de ello. Vamos a comenzar visualizando en una tabla aquellas filas identificadas con valores duplicados en la variable Nombre:
nombres_duplicados <- resultadosTokyo2025 %>%
group_by(Nombre) %>%
filter(n() > 1) %>%
ungroup() %>%
arrange(Nombre)
# Dimensiones
cat("Total de filas con nombres duplicados:", nrow(nombres_duplicados), "\n")Total de filas con nombres duplicados: 488
cat("Cantidad de nombres únicos duplicados:", n_distinct(nombres_duplicados$Nombre), "\n")Cantidad de nombres únicos duplicados: 231
datatable(
nombres_duplicados,
options = list(
pageLength = 10, # Muestra 10 filas por página
scrollX = TRUE, # Habilita el scroll horizontal
dom = 'tip' # Muestra la tabla, información ("Showing...") y paginación
),
rownames = FALSE # Quita los números de fila
)De esta tabla y resumen podemos observar lo siguiente:
- Existen valores nulos ocultos identificados cómo (** ** / *** ***)
- Existen nombres duplicados entre los corredores pero, tanto el BIB como los tiempos parciales son completamente distintos. Por lo tanto son datos consistentes.
Vamos a proceder a determinar como NA’s reconocidos por el lenguaje estos valores nulos ocultos:
# Eliminamos los datos recién creados:
rm(nombres_duplicados)
# Modificamos el df inicial añadiendo estos datos faltantes.
resultadosTokyo2025 <- resultadosTokyo2025 %>% mutate(Nombre = na_if(Nombre, "*** *** / *** ***"))Una vez identificados los datos nulos ocultos de la variable Nombre, vamos a continuar con la variable Nacionalidad, como bien hemos detectado en Sección 4.1, esta variable toma valores “-” que corresponden a corredores a los que les falta el dato de la nacionalidad. Vamos a modificar este valor para tener identificados los datos nulos correspondientes a esta variable:
resultadosTokyo2025<- resultadosTokyo2025 %>% mutate(Nacionalidad = na_if(Nacionalidad, "-"))4.1.2 Valores Atípicos Lógicos
Tras una revisión preliminar de las variables numéricas en la sección Sección 4.1, los datos de tiempos finales y parciales no presentan valores evidentemente imposibles, como registros negativos o duraciones extrañas.
Sin embargo, es crucial verificar la integridad lógica secuencial de los datos. Por ello, esta sección se centrará en detectar parciales inconsecuentes. Este tipo de anomalía ocurre cuando el tiempo acumulado en un punto de control es erróneamente menor que el del punto de control anterior (es decir, tiempo_parcial_X+1 < tiempo_parcial_X), lo cual es físicamente imposible y señala un error en la captura o registro de los datos.
inconsistentes_parciales <- resultadosTokyo2025 %>%
filter(
parcial_5km >= parcial_10km |
parcial_10km >= parcial_15km |
parcial_15km >= parcial_20km |
parcial_20km >= medio_maraton |
medio_maraton >= parcial_25km |
parcial_25km >= parcial_30km |
parcial_30km >= parcial_35km |
parcial_35km >= parcial_40km |
parcial_40km >= tiempo_oficial
)
dim(inconsistentes_parciales)[1] 0 15
Observamos que el filtrado recibe 0 filas que cumplan que un parcial posterior tenga menor tiempo acumulado que uno anterior, por lo tanto no existen valores atípicos lógicos sobre estas variables.
rm(inconsistentes_parciales)4.1.3 Valores Nulos y Faltantes
En esta subsección se realizará un análisis de los valores nulos y faltantes que existen en el conjunto de datos con el objetivo de identificar posibles patrones y establecer una estrategia para tratarlos.
Para ello, comenzamos realizando un análisis de completitud por variable:
naniar::miss_var_summary(resultadosTokyo2025)# A tibble: 15 × 3
variable n_miss pct_miss
<chr> <int> <num>
1 parcial_25km 14 0.0387
2 parcial_40km 11 0.0304
3 parcial_5km 10 0.0276
4 parcial_15km 9 0.0249
5 parcial_20km 9 0.0249
6 parcial_30km 8 0.0221
7 parcial_10km 7 0.0194
8 Nombre 5 0.0138
9 parcial_35km 5 0.0138
10 Nacionalidad 4 0.0111
11 medio_maraton 4 0.0111
12 BIB 0 0
13 Genero 0 0
14 Edad 0 0
15 tiempo_oficial 0 0
Se puede observar que tenemos un porcentaje total de datos nulos extremadamente bajo en cada variable. Se pueden observar que los datos nulos se agrupan en:
- Datos nulos en la variable Nombre
- Datos nulos en la variable Nacionalidad
- Datos nulos en las variables referentes a Parciales
En los dos primeros casos, se ha decidido no realizar ninguna imputación ni tampoco eliminar las filas afectadas. Estos registros no se considerarán a la hora de realizar estudios univariantes o bivariantes que las involucren, manteniendo así información de especial relevancia en las demás variables.
Respecto al último caso, vamos a realizar un estudio para entender qué forma tienen los datos nulos asociados a los Parciales, para ello, comenzamos visualizando las filas con algún NA en estas filas:
# Definimos en un vector las columnas de parciales:
cols_tiempo <- c("parcial_5km", "parcial_10km", "parcial_15km", "parcial_20km", "medio_maraton", "parcial_25km", "parcial_30km", "parcial_35km", "parcial_40km")
# Definimos las distancias asociadas a cada columna de parciales:
dist <- c(5, 10, 15, 20, 21.0975, 25, 30, 35, 40)
#Filas con NA
filas_con_na <- resultadosTokyo2025[ rowSums(is.na(resultadosTokyo2025[cols_tiempo])) > 0, ]
# Mostramos el dataframe formado por todas las filas que tienen al menos un valor faltante:
datatable(
filas_con_na,
options = list(
pageLength = 10, # Muestra 10 filas por página
scrollX = TRUE, # Habilita el scroll horizontal
dom = 'tip' # Muestra la tabla, información ("Showing...") y paginación
),
rownames = FALSE # Quita los números de fila
)Antes de seguir trabajando sobre el dataframe, es conveniente hacer una copia para trabajar sobre la copia sin tocar los datos originales por los erroes que pueda haber.
df_trabajo <- resultadosTokyo2025Tras ello, vamos a visualizar, de cada fila, cuántos valores nulos en total tiene:
filas_con_na <- df_trabajo[rowSums(is.na(df_trabajo[, cols_tiempo])) > 0, ]
filas_con_na$n_na <- rowSums(is.na(filas_con_na[, cols_tiempo]))
unique(filas_con_na$n_na)[1] 9 1 3 8 2 5
Por lo que se puede observar, el número de parciales faltantes puede ser 1, 2, 3, 5, 8 ó 9. Para finalizar el estudio de la forma de los valores nulos y faltantes de nuestro conjunto de datos, vamos a visualizar de todas estas filas, el número de parciales faltantes consecutivos, ya que será de vital importancia posterior.
filas_con_na$max_na_consec <- sapply(1:nrow(filas_con_na), function(i) {
elems <- unlist(filas_con_na[i, cols_tiempo])
na_vec <- is.na(elems)
if(all(!na_vec)) return(0)
max(rle(na_vec)$lengths[rle(na_vec)$values == TRUE])
})
unique(filas_con_na$max_na_consec)[1] 9 1 2 4 3
Podemos observar viendo los valores que toma esta variable que, el número de parciales consecutivos faltantes puede ser 1,2,3,4 o 9.
4.2 Limpieza y Corrección de Datos
En esta sección se realizará la correspondiente limpieza y correción de datos.
4.2.1 Tratamiento de Valores Nulos
Vamos a realizar el correspondiente tratamiento de los valores nulos, como bien hemos adelantado en la sección Sección 4.1.3, los datos faltantes correspondientes a las variables de Nombre y Nacionalidad ni se imputarán ni se eliminarán dichas filas.
Respecto a la estrategia para los parciales, cabe destacar que ninguno de las filas corresponde a abandonos o descalificaciones ya que sí que existe su tiempo final al paso por la meta. Se ha decidido tratar los datos nulos de la siguiente manera:
- Si una fila tiene 2 o más parciales sin tiempo registrado consecutivos, no se considerará imputable debido a que supone un desconocimiento de 15 kilómetros. Asumir un ritmo constante durante 15 kilómetros en una disciplina de duración tan larga como la maratón en atletas amateur (son los que tienen parciales sin registrar) es irreal.
Por lo tanto, de las 43 filas con datos nulos que teníamos, vamos a imputar las siguientes:
filas_con_na$clasificacion <- ifelse(
filas_con_na$max_na_consec >= 2,
"descartar",
"imputable"
)
filas_con_na$descarte <- filas_con_na$clasificacion == "descartar"
filas_para_imputar <- filas_con_na[filas_con_na$descarte == FALSE, ]
datatable(
filas_para_imputar,
options = list(
pageLength = 10, # Muestra 10 filas por página
scrollX = TRUE, # Habilita el scroll horizontal
dom = 'tip' # Muestra la tabla, información ("Showing...") y paginación
),
rownames = FALSE # Quita los números de fila
)Es decir, se produce un descarte de 9 filas del conjunto de datos, ya que tienen más de dos parciales consecutivos con datos faltantes. Respecto a las filas que sí son imputables, hay que tener en cuenta la forma en la que lo vamos a realizar dependiendo cuál sea el parcial faltante:
- Si el dato faltante es parcial_5km, se imputará teniendo en cuenta que el ritmo en el primer tramo de la maratón es, en promedio un 5% más rápido que el ritmo de tramos posteriores.
- Si el dato faltante es parcial_40km, habrá que considerar que desde el parcial 35 hay 5km de distancia pero para la llegada a la meta hay 2km y 195m.
- Si el dato faltante es media_maraton, es preciso saber que desde el parcial del kilómetro 20 hay 1km y 96m de distancia y 3km 904m hasta el kilómetro 25.
- Si el dato faltante es cualquier otro parcial, se imputará calculando el tiempo de paso con los parciales inmediatamente anterior y posterior a él.
Vamos a comenzar por ello, descartando del conjunto de datos las filas que hemos clasificado como no imputables, lo haremos realizando un cruce por el BIB (único) y filtrando aquellas filas cuya clasificación sea “descartar”:
bibs_a_descartar <- filas_con_na %>%
filter(clasificacion == "descartar") %>%
select(BIB)
resultadosTokyo2025<- resultadosTokyo2025 %>%
anti_join(bibs_a_descartar, by = "BIB")
rm(bibs_a_descartar, filas_con_na)Una vez descartados aquellos corredores no imputables, vamos a realizar la imputación de aquellos que sí que es posible realizarla. Para ello, vamos a comenzar imputando aquellos tiempos que no sean ni parcial_5km ni parcial_40km, para ello, creamos la siguiente función que gracias a una interpolación lineal estándar, nos ayudará a calcular el tiempo que necesitamos imputar con los tiempos y distancias previo y posterior:
imputar_parciales <- function(tiempos, dist, cols_tiempo) {
if (!inherits(tiempos, "hms")) tiempos <- hms::as_hms(tiempos)
for (i in seq_along(tiempos)) {
if (is.na(tiempos[i])) {
prev_idx <- max(which(!is.na(tiempos[1:(i - 1)])), na.rm = TRUE)
next_idx <- min(which(!is.na(tiempos[(i + 1):length(tiempos)])), na.rm = TRUE)
if (is.finite(next_idx)) next_idx <- next_idx + i
if (is.finite(prev_idx) && is.finite(next_idx)) {
t_prev <- tiempos[prev_idx]
t_next <- tiempos[next_idx]
d_prev <- dist[prev_idx]
d_next <- dist[next_idx]
d_missing <- dist[i]
tiempos[i] <- t_prev + (t_next - t_prev) * ((d_missing - d_prev) / (d_next - d_prev)) #interpolación lineal estándar
}
}
}
return(tiempos)
}
primera_imputacion <- as.data.frame(
t(apply(filas_para_imputar[cols_tiempo], 1, imputar_parciales, dist = dist, cols_tiempo = cols_tiempo))
)
colnames(primera_imputacion) <- cols_tiempo
primera_imputacion[cols_tiempo] <-
lapply(primera_imputacion[cols_tiempo], hms::as_hms)Tras la imputación correcta de los parciales intermedios, añado la imputación de aquellos valores faltantes a los 5km, que se realizará calculando el ritmo medio estimando con el parcial de los 10km, y multiplicándolo por un factor salida de un 95%, es decir, que el tiempo de paso por los 5km es un 5% más rápido. Para ello:
#Añadir a primera_imputacion la imputacion de parcial_5km
idx_5 <- which(cols_tiempo == "parcial_5km")
idx_10 <- which(cols_tiempo == "parcial_10km")
filas_na_5 <- which(is.na(primera_imputacion$parcial_5km))
d_5 <- dist[idx_5]
d_10 <- dist[idx_10]
factor_salida <- 0.95 # 5% más rápida
primera_imputacion$parcial_5km[filas_na_5] <- hms::as_hms(
(primera_imputacion$parcial_10km[filas_na_5] - hms::as_hms(0)) * factor_salida * (d_5 / d_10)
)Para la imputación del parcial de 40km, se ha decidido usar el ritmo justamente anterior, es decir, el tiempo que tardó al paso por el parcial del 30 al 35, es el mismo del 35 al 40, por lo tanto se suma esa diferencia de tiempo para obtener el tiempo de paso por el kilómetro 40 de la prueba.
#Añadir a primera_imputacion la imputacion del parcial_40km
idx_35 <- which(cols_tiempo == "parcial_35km")
idx_40 <- which(cols_tiempo == "parcial_40km")
d_35 <- dist[idx_35]
d_40 <- dist[idx_40]
filas_na_40 <- which(is.na(primera_imputacion$parcial_40km))
primera_imputacion$parcial_40km[filas_na_40] <- hms::as_hms(
primera_imputacion$parcial_35km[filas_na_40] +
(primera_imputacion$parcial_35km[filas_na_40] - primera_imputacion$parcial_30km[filas_na_40]) *
((d_40 - d_35) / (d_35 - dist[idx_35 - 1]))
)Tras ello, redondeamos los tiempos del formato hms:
redondear_hms <- function(x) {
seg_total <- as.numeric(x)
seg_total_rounded <- round(seg_total)
hms::as_hms(seg_total_rounded)
}
primera_imputacion[cols_tiempo] <- lapply(
primera_imputacion[cols_tiempo],
redondear_hms
)Por último, añadimos las imputaciones correspondientes al conjunto de datos inicial:
filas_para_imputar[rownames(filas_para_imputar), cols_tiempo] <- primera_imputacion
# Elimino las columnas creadas.
filas_para_imputar <- filas_para_imputar[, !(names(filas_para_imputar) %in% c("n_na", "max_na_consec", "clasificacion", "descarte"))]
# Cargo los datos imputados actualizando las filas correspondientes.
resultadosTokyo2025 <- resultadosTokyo2025 %>%
rows_update(filas_para_imputar, by = "BIB")
print("Datos faltantes imputados")[1] "Datos faltantes imputados"
# Elimino todos los valores, datos y funciones creadas:
rm(df_trabajo, filas_para_imputar, primera_imputacion, d_5, d_10, d_35, d_40, dist, factor_salida, filas_na_5, filas_na_40, idx_5, idx_10, idx_35, idx_40, imputar_parciales, redondear_hms)Por último, observo el análisis de completitud para ver cómo se ha quedado el conjunto de datos:
skimr::skim(resultadosTokyo2025)| Name | resultadosTokyo2025 |
| Number of rows | 36164 |
| Number of columns | 15 |
| _______________________ | |
| Column type frequency: | |
| character | 3 |
| difftime | 10 |
| numeric | 2 |
| ________________________ | |
| Group variables | None |
Variable type: character
| skim_variable | n_missing | complete_rate | min | max | empty | n_unique | whitespace |
|---|---|---|---|---|---|---|---|
| Nombre | 5 | 1 | 11 | 81 | 0 | 35906 | 0 |
| Nacionalidad | 4 | 1 | 2 | 24 | 0 | 126 | 0 |
| Genero | 0 | 1 | 6 | 19 | 0 | 3 | 0 |
Variable type: difftime
| skim_variable | n_missing | complete_rate | min | max | median | n_unique |
|---|---|---|---|---|---|---|
| tiempo_oficial | 0 | 1 | 7403 secs | 25218 secs | 16573.0 secs | 13775 |
| parcial_5km | 0 | 1 | 865 secs | 3855 secs | 1707.5 secs | 1584 |
| parcial_10km | 0 | 1 | 1735 secs | 6491 secs | 3399.0 secs | 2980 |
| parcial_15km | 0 | 1 | 2610 secs | 8654 secs | 5107.0 secs | 4407 |
| parcial_20km | 0 | 1 | 3487 secs | 11581 secs | 6864.0 secs | 5954 |
| medio_maraton | 0 | 1 | 3678 secs | 12197 secs | 7256.0 secs | 6299 |
| parcial_25km | 0 | 1 | 4363 secs | 14527 secs | 8664.0 secs | 7523 |
| parcial_30km | 0 | 1 | 5242 secs | 17334 secs | 10614.0 secs | 9152 |
| parcial_35km | 0 | 1 | 6133 secs | 20184 secs | 12684.0 secs | 10616 |
| parcial_40km | 0 | 1 | 7019 secs | 23424 secs | 14894.0 secs | 12102 |
Variable type: numeric
| skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
|---|---|---|---|---|---|---|---|---|---|---|
| BIB | 0 | 1 | 29347.79 | 11716.84 | 1 | 19745.75 | 29416.5 | 39084.25 | 91050 | ▃▇▆▁▁ |
| Edad | 0 | 1 | 46.59 | 11.07 | 19 | 38.00 | 47.0 | 55.00 | 84 | ▂▆▇▃▁ |
4.3 Estandarización de Formatos
En esta sección se realizará una estandarización de formatos en los valores de algunas columnas.
4.3.1 Género de los participantes
Debido a que los datos han sido extraídos de una web japonesa, los valores de la columna Genero presentan un formato bilingüe, con caracteres tanto japoneses como ingleses, separados por el caracter /. Para facilitar posteriores análisis y visualizaciones, se ha tomado la decisión de unificar esta variable a un único idioma, en este caso, el inglés. Una vez limpia, la convertimos a factor. Para ello:
resultadosTokyo2025 <- resultadosTokyo2025 %>%
mutate(
# Modificamos la columna 'Género'
Genero = str_remove(Genero, "^.*/")
)
resultadosTokyo2025$Genero <- as.factor(resultadosTokyo2025$Genero)4.3.2 Nombre de los participantes
Al igual que el Genero, la variable Nombre presenta los nombres de cada participante con el mismo formato bilingüe, es por ello que también nos quedaremos con el idioma inglés, ya que esto nos abre la posibilidad de poder cruzar datos con otros maratones si en algún momento del análisis queremos hacer un análisis longitudinal de algún deportista.
# Versión compacta para reemplazar la columna original
resultadosTokyo2025 <- resultadosTokyo2025 %>%
mutate(
Nombre = coalesce(str_trim(str_split_i(Nombre, "/", 2)), Nombre)
)4.3.3 Códigos de Nacionalidad
Con el objetivo de poder definir de manera estándar y acorde a códigos para poder graficar más adelante países, se ha decidido estandarizar la variable Nacionalidad, ya que se puede observar cómo contiene carácteres especiales japonenes o incluso regiones cuyo nombre de país es distinto, es por ello que visualizamos primeramente los nombres:
conteo_paises <- resultadosTokyo2025 %>%
count(Nacionalidad, sort = TRUE, name = "Numero_Corredores")
# Lo visualizamos con la libreria DT porque es más interactiva a la hora de generar el documento qmd.
datatable(
conteo_paises,
options = list(
pageLength = 10,
scrollX = TRUE
),
rownames = FALSE, # Elimina los números de fila
colnames = c("País", "Número de Corredores")
)Se peude observar que no existen países con el nombre duplicado (es decir, varios nombres para el mismo país).
Vamos a crear una nueva variable en nuestro conjunto de datos que se relacione con el país con el nombre ya estandarizado, Pais_Estandarizado.
resultadosTokyo2025 <- resultadosTokyo2025 %>%
mutate(
# Primero, manejamos el caso del guion para que no interfiera
Nacionalidad = na_if(Nacionalidad, "-"),
# Ahora, creamos la nueva columna estandarizada
Pais_Estandarizado = case_when(
Nacionalidad == "日本" ~ "Japan",
Nacionalidad == "PR OF CHINA" ~ "China",
Nacionalidad == "CHINESE TAIPEI" ~ "Taiwan",
Nacionalidad == "GREAT BRITAIN & N.I." ~ "United Kingdom",
Nacionalidad == "HONG KONG, CHINA" ~ "Hong Kong",
Nacionalidad == "KOREA" ~ "Korea, South",
Nacionalidad == "DPR OF KOREA" ~ "Korea, North",
Nacionalidad == "F Y REP. OF MACEDONIA" ~ "Macedonia",
Nacionalidad == "SLOVAK REPUBLIC" ~ "Slovakia",
Nacionalidad == "ISLAMIC REPUBLIC OF IRAN" ~ "Iran",
Nacionalidad == "MACAO" ~ "Macau",
Nacionalidad == "SAINT KITTS AND NEVIS" ~ "Saint Kitts and Nevis",
Nacionalidad == "BAHAMAS" ~ "Bahamas, The",
Nacionalidad == "REPUBLIC Of YEMEN" ~ "Yemen",
Nacionalidad == "UNITED STATES" ~ "United States", # Usar el nombre completo suele ser más compatible
# Para todos los demás países los ponemos en el formato minúsculas con la primera letra en mayúsculas.
TRUE ~ str_to_title(Nacionalidad)
)
)
conteo_paises_limpios <- resultadosTokyo2025 %>%
count(Pais_Estandarizado, sort = TRUE)Observamos que, en principio, los errores están completamente corregidos. Para ver si los nuevos nombres estandarizados están correctamente, vamos a cruzar los países recién creados con un conjunto de datos muy usado para realizar luego mapas geográficos con la librería Plotly.
df <- read.csv("https://raw.githubusercontent.com/plotly/datasets/master/2014_world_gdp_with_codes.csv")
paises_validos <- df %>%
distinct(COUNTRY) %>%
# Renombramos la columna para que coincida con la nuestra y poder hacer el join
rename(Pais_Estandarizado = COUNTRY)
paises_problematicos <- resultadosTokyo2025 %>%
filter(!is.na(Pais_Estandarizado)) %>% # Ignoramos los NA que puedan existir
distinct(Pais_Estandarizado) %>%
anti_join(paises_validos, by = "Pais_Estandarizado")
dim(paises_problematicos)[1] 2 1
paises_problematicos$Pais_Estandarizado[1] "Palestine" "Myanmar"
Se observan sólamente 2 países problemáticos (Palestine y Myanmar) por lo tanto, creamos una nueva variable llamada CODE_PLOTLY que contendrá el código de cada país según este dataframe subido:
resultadosTokyo2025 <- resultadosTokyo2025 %>%
left_join(
df %>% select(COUNTRY, CODE),
by = c("Pais_Estandarizado" = "COUNTRY")
)rm(conteo_paises, conteo_paises_limpios, df, paises_problematicos, paises_validos)4.3.4 Conversión de Tiempos
Con el objetivo de poder conseguir mejores análisis tanto univariantes como bivariantes en las variables referentes a tiempos (parciales o finales), se ha decidido convertirlas a segundos (numérico) para poder tratarlas adecuadamente de forma estadística. Cabe destacar que siempre que queramos visualizar las horas, minutos y segundos, podremos volver a convertirlas al formato adecuado.
resultadosTokyo2025 <- resultadosTokyo2025 %>%
mutate(
across(where(is_hms), as.numeric)
)4.4 Creación de Variables Derivadas
Con el objetivo de lograr una mayor comprensión del conjunto de datos inicial a través de los posteriores estudios, se ha tomado la decisión de crear algunas nuevas variables.
4.4.1 Ritmos Parciales
Se van a crear nuevas variables para determinar el ritmo (min/km) de los corredores al paso de cada uno de los parciales de la prueba. Además se creará una variable con el ritmo promedio basado en el tiempo_final.
Para ello:
# Creacion de la variable ritmo_oficial:
resultadosTokyo2025 <- resultadosTokyo2025 %>%
mutate(
ritmo_oficial = hms(seconds = round((tiempo_oficial) / 42.195)),
ritmo_5km = hms(seconds = round((parcial_5km) / 5)),
ritmo_10km = hms(seconds = round((parcial_10km - parcial_5km) / 5)),
ritmo_15km = hms(seconds = round((parcial_15km - parcial_10km) / 5)),
ritmo_20km = hms(seconds = round((parcial_20km - parcial_15km) / 5)),
ritmo_25km = hms(seconds = round((parcial_25km - parcial_20km) / 5)),
ritmo_30km = hms(seconds = round((parcial_30km - parcial_25km) / 5)),
ritmo_35km = hms(seconds = round((parcial_35km - parcial_30km) / 5)),
ritmo_40km = hms(seconds = round((parcial_40km - parcial_35km) / 5))
)4.4.2 Categorización por Nivel de Rendimiento
Con el principal objetivo de poder analizar posteriormente de una manera más enriquecida los posibles grupos de carrera, se ha decidido crear una nueva variable que determinará el nivel del atleta.
Para definir los umbrales que diferenciarán dicho nivel se ha atendido primeramente a la marca mínima para participar en el Campeonato del Mundo. Establecida la élite, para la creación de las categorías de “Alto nivel”, “Muy entrenado/a” y “Moderadamente entrenado/a” y “Principiante” se han observado las marcas con que las Majors organizan los cajones de salida.
Los niveles han resultado así:
- Élite - < 02:17:00 (Men(M)) / < 02:43:00 (Women(W))
- Alto nivel - < 02:30:00 (Men(M)) / < 03:00:00 (Women(W))
- Muy entrenado/a - < 03:00:00 (Men(M)) / < 03:30:00 (Women(W))
- Moderadamente entrenado/a - < 03:30:00 (Men(M)) / < 04:00:00 (Women(W))
- Principiante - + 03:30:00 (Men(M)) / + 04:00:00 (Women(W))
resultadosTokyo2025<- resultadosTokyo2025 |>
mutate(
categoria = case_when(
#hombres
Genero == "Men" & tiempo_oficial < as.numeric(as_hms("02:17:00")) ~ "Élite",
Genero == "Men" & tiempo_oficial < as.numeric(as_hms("02:30:00")) ~ "Alto nivel",
Genero == "Men" & tiempo_oficial < as.numeric(as_hms("03:00:00")) ~ "Muy entrenado",
Genero == "Men" & tiempo_oficial < as.numeric(as_hms("03:30:00")) ~ "Moderadamente entrenado",
Genero == "Men" & tiempo_oficial >= as.numeric(as_hms("03:30:00")) ~ "Principiante",
#mujeres
Genero == "Women" & tiempo_oficial < as.numeric(as_hms("02:43:00")) ~ "Élite",
Genero == "Women" & tiempo_oficial < as.numeric(as_hms("03:00:00")) ~ "Alto nivel",
Genero == "Women" & tiempo_oficial < as.numeric(as_hms("03:30:00")) ~ "Muy entrenado",
Genero == "Women" & tiempo_oficial < as.numeric(as_hms("04:00:00")) ~ "Moderadamente entrenado",
Genero == "Women" & tiempo_oficial >= as.numeric(as_hms("04:00:00")) ~ "Principiante",
TRUE ~ "No élite"
),
categoria = factor(categoria, levels = c("Élite", "Alto nivel", "Muy entrenado", "Moderadamente entrenado","Principiante", "No élite"))
)5 Análisis Univariante
Bajo este análisis se examinará cada variable del conjunto de datos para resumir su distribución, principales características y tendencias. Este tipo de análisis se centra en una sola variable a la vez, sin tener en cuenta su relación con otras, y permite obtener una primera descripción general de los datos. A continuación se revisa cada variable del dataset para observar sus principales características estadísticas.
5.1 Edad
Esta variable indica la edad de los corredores que participaron en la maratón. Se trata de una variable numérica discreta. Primero se examinán los principales estadísticos de centralización: media, moda y mediana
# Fun. moda que devuelve el valor más frecuente (si hay empates devuelve el primero)
moda <- function(x) {
x <- x[!is.na(x)]
if (length(x) == 0) return(NA_real_)
ux <- unique(x)
ux[which.max(tabulate(match(x, ux)))]
}
# Estadísticos resumidos (redondeados)
edad_stats <- resultadosTokyo2025 %>%
summarise(
Total_valores = sum(!is.na(Edad)),
Media = round(mean(Edad, na.rm = TRUE), 2),
Mediana = median(Edad, na.rm = TRUE),
Moda = moda(Edad),
)
datatable(
edad_stats,
options = list(dom = 't'),
rownames = FALSE
)La edad media de los corredores que participaron en la maratón de Tokyo 2025 es de aproximadamente 46 años, con una mediana de 47 años y una moda de 50 años. Esto indica que la distribución de edades está muy ligeramente sesgada hacia edades mayores, ya que la media es menor que la mediana. La moda sugiere que la edad más común entre los corredores es de 50 años, lo que podría indicar una mayor participación de corredores en este rango de edad.
Representado de forma gráfica, en la siguiente figura se observa un histograma con línea de densidad.
ggplot(resultadosTokyo2025, aes(x = Edad)) +
geom_histogram(aes(y = after_stat(density)), binwidth=2, fill = "#90CAF9", color = "gray30") +
geom_density(aes(y = after_stat(density)),
fill = "#1976D2", alpha = 0.15) +
labs(title = "Distribución de Edad",
x = "Edad (años)", y = "Densidad") +
theme_minimal(base_size = 12)cat(
"Skewness:", skewness(resultadosTokyo2025$Edad, na.rm = TRUE), "\n",
"Kurtosis:", kurtosis(resultadosTokyo2025$Edad, na.rm = TRUE)
)Skewness: -0.05998237
Kurtosis: -0.5790181
Combinando los resultados de la gráfica con los valores de Skewness y Kurtosis, la variable edad presenta una asimetría mínima (skewness ≈ -0.06), indicando una distribución prácticamente simétrica. Su kurtosis negativa (-0.58) muestra que la distribución es ligeramente más plana que una normal, con colas algo menos pesadas. En conjunto, la distribución puede considerarse aproximadamente normal para fines de análisis estadístico.
La mayoría de los corredores tienen edades comprendidas entre los 25 y 40 años, con un pico alrededor de los 30 años.
Nota: La función kurtosis() del paquete e1071 calcula la excess kurtosis, es decir, la curtosis menos 3. La interpretamos con el objetivo de ver si es normal ya que un valor de curtosis = 3 indicaría que sus colas son idénticas a la distribución normal
A continuación se presentan los estadísticos de dispersión y forma de la distribución de edades.
edad_disp_forma <- resultadosTokyo2025 %>%
summarise(
Minimo = min(Edad, na.rm = TRUE),
Maximo = max(Edad, na.rm = TRUE),
Rango = max(Edad, na.rm = TRUE) - min(Edad, na.rm = TRUE),
Varianza = round(var(Edad, na.rm = TRUE), 2),
Desviación_Estandar = round(sd(Edad, na.rm = TRUE), 2),
Desviacion_tipica = round(sqrt(var(Edad, na.rm = TRUE)), 2),
Coeficiente_Variacion = round((sd(Edad, na.rm = TRUE) / mean(Edad, na.rm = TRUE)) * 100, 2),
)
datatable(
edad_disp_forma,
options = list(dom = 't'),
rownames = FALSE
)Las edades varían desde un mínimo de 19 años hasta un máximo de 84 años, con un rango total de 65 años. La varianza (122,64) y la desviación estándar (11,07) indican que hay una dispersión moderada en las edades de los corredores. El coeficiente de variación del 23,77% sugiere que la variabilidad relativa de las edades es considerable.
En la siguiente grafica se observa un boxplot que muestra la dispersión de las edades de los corredores.
ggplot(resultadosTokyo2025, aes(y = Edad)) +
geom_boxplot(fill = "#90CAF9", color = "gray30", outlier.color = "red", outlier.size = 1.5) +
labs(title = "Boxplot de Edad",
y = "Edad (años)") +
theme_minimal(base_size = 12)El boxplot muestra que la mayoría de las edades de los corredores están concentradas entre aproximadamente 39 y 55 años. La mediana (línea dentro de la caja) está cerca del centro de la caja, lo que indica una distribución mas o menos simétrica de las edades. Los bigotes del boxplot indican que no hay valores extremadamente alejados del rango intercuartílico, lo que sugiere una distribución relativamente homogénea de las edades.
rm(edad_disp_forma, edad_stats)5.2 Género
En esta sección se realiza un análisis descriptivo de la variable Genero. Para ello, vamos a comenzar inspeccionando las categorías de la variable y sus frecuencias, tanto absoluta (número de corredores) como relativa (porcentaje del total).
tabla_frecuencias_genero <- resultadosTokyo2025 %>%
count(Genero, name = "Frecuencia_Absoluta") %>%
mutate(
Porcentaje = Frecuencia_Absoluta / sum(Frecuencia_Absoluta) * 100
) %>%
arrange(desc(Frecuencia_Absoluta))
kable(
tabla_frecuencias_genero,
col.names = c("Género", "Nº de Corredores", "Porcentaje (%)"),
digits = c(0, 0, 2),
caption = "Tabla: Distribución de participantes por género."
)| Género | Nº de Corredores | Porcentaje (%) |
|---|---|---|
| Men | 26701 | 73.83 |
| Women | 9425 | 26.06 |
| Non-binary | 38 | 0.11 |
Se puede observar que existen 3 categorías con la siguiente participación:
- Men:
- Women
- Non-Binary
A continuación vamos a graficar la información obtenida en la tabla de frecuencia a través de un gráfico de barras.
ggplot(tabla_frecuencias_genero, aes(x = reorder(Genero, -Frecuencia_Absoluta), y = Frecuencia_Absoluta, fill = Genero)) +
geom_bar(stat = "identity", show.legend = FALSE) +
geom_text(
aes(label = scales::comma(Frecuencia_Absoluta)),
vjust = -0.5,
size = 3.5,
color = "black"
) +
scale_fill_viridis_d() +
# Títulos, subtítulos y etiquetas de los ejes
labs(
title = "Distribución de Participantes por Género",
subtitle = "Maratón de Tokyo | 02-03-2025",
x = "Género",
y = "Número de Corredores"
) +
# Tema limpio y ajuste de eje para dar espacio a las etiquetas
theme_minimal(base_size = 14) + # Aumenté un poco el tamaño base de la letra
scale_y_continuous(expand = expansion(mult = c(0, 0.1))) +
theme(
plot.title = element_text(face = "bold", size = 18), # Título en negrita y más grande
plot.subtitle = element_text(size = 12, color = "gray30") # Subtítulo más sutil
)Por lo tanto podemos observar que la composición de la maratón muestra que casi tres de cada cuatro corredores eran hombres.
rm(tabla_frecuencias_genero)5.3 Nacionalidad
A continuación, se realizará un análisis univariante de la Nacionalidad de los corredores, para ello, vamos a realizar un mapa interactivo con la librería Plotly en el que se podrá visualizar el número de participantes por país en una escala de colores:
# 1. Contar corredores (asumimos que ya tienes esto)
frecuencia_paises <- resultadosTokyo2025 %>%
count(Pais_Estandarizado, CODE, name = "corredores", sort = TRUE)
# 2. Crear una columna para el logaritmo de corredores
frecuencia_paises <- frecuencia_paises %>%
mutate(corredores_log = log10(corredores + 1))
# 3. Crear mapa interactivo con la escala logarítmica
fig <- plot_ly(
frecuencia_paises,
type = 'choropleth',
locations = ~CODE,
# Z: La variable que define el color ahora es el logaritmo
z = ~corredores_log,
# Text: Mantenemos el nombre del país para el hover
text = ~Pais_Estandarizado,
# Customdata: Guardamos el número real de corredores para mostrarlo en el hover
customdata = ~corredores,
# Elige una paleta de colores que resalte bien. "Viridis", "Plasma" o "YlOrRd" son buenas opciones.
colorscale = "Viridis",
reversescale = TRUE, # Invertir la escala para que más sea más oscuro
colorbar = list(title = "Corredores (Escala Log)"),
# Hovertemplate: Mostramos el número real de participantes usando 'customdata'
hovertemplate = "<b>%{text}</b><br>Participantes: %{customdata}<extra></extra>"
) %>%
layout(
title = "Distribución de Corredores por País (Maratón de Tokio)",
geo = list(showframe = FALSE, showcoastlines = FALSE, projection = list(type = 'mercator'))
)
figEn este mapa se puede observar que el país que más participantes tuvo fue Japón, seguramente debido a que dicha maratón se realizó en este país. Otros países que llevaron más de 1000 corredores fueron Estados Unidos, Taiwán, China e Inglaterra.
rm(frecuencia_paises, fig)5.4 Tiempo Oficial
En el caso de las variables de tiempo, se realizará el análisis con las columnas transformadas a segundos pero reflejando los resultados en formato horas, minutos y segundos para una mejor interpretación. Esta transformación implica que la varible es numérica continua. Como en variables numéricas anteriores, se comenzará con los estadísticos de centralización.
# Estadísticos resumidos (redondeados)
tiempo_stats <- resultadosTokyo2025 %>%
summarise(
Total_valores = sum(!is.na(tiempo_oficial)),
Media = round(mean(tiempo_oficial, na.rm = TRUE), 2),
Mediana = median(tiempo_oficial, na.rm = TRUE),
Moda = moda(tiempo_oficial),
)
# Convertir a hms para mejor interpretación
tiempo_stats <- tiempo_stats %>%
mutate(
Media = as_hms(Media),
Mediana = as_hms(Mediana),
Moda = as_hms(Moda)
)
datatable(
tiempo_stats,
options = list(dom = 't'),
rownames = FALSE
)La media del tiempo oficial de los corredores que participaron en la maratón de Tokyo 2025 es de aproximadamente 4 horas, 40 minutos y 23 segundos, lo que sugiere que es una prueba dominada por atletas amateurs en números de participación. Con una mediana de 4 horas, 36 minutos y 13 segundos. Esto indica que la distribución de los tiempos está sesgada positivamente, ya que la media es mayor que la mediana, es decir, hay corredores con tiempos significativamente más altos que elevan la media. La moda sugiere que el tiempo más común entre los corredores es de 4 horas, 15 minutos y 30 segundos.
Representado de forma gráfica, en la siguiente figura se observa un histograma con línea de densidad.
# Convierte a hms para mejor interpretación
ggplot(resultadosTokyo2025, aes(x = tiempo_oficial)) +
geom_histogram(aes(y = ..density..), binwidth=500, fill = "#90CAF9", color = "gray30") +
geom_density(aes(y = ..density..),
fill = "#1976D2", alpha = 0.15) +
scale_x_continuous(
breaks = seq(0, 21600, by = 3600), # cada 30 minutos
labels = function(x) as_hms(x) # convierte a hms para etiquetas
) +
labs(title = "Distribución del Tiempo Oficial",
x = "Tiempo Oficial (hh:mm:ss)", y = "Densidad") +
theme_minimal(base_size = 12)En esta gráfica se observa que la distribución de los tiempos oficiales de los corredores es asimétrica positivamente (hacia la derecha). La mayoría de los corredores tienen tiempos comprendidos entre 3 horas y 30 minutos y 5 horas, con un pico alrededor de las 4 horas.
A continuación se presentan los estadísticos de dispersión y forma de la distribución de tiempos oficiales.
tiempo_disp_forma <- resultadosTokyo2025 %>%
summarise(
Minimo = as_hms(min(tiempo_oficial, na.rm = TRUE)),
Maximo = as_hms(max(tiempo_oficial, na.rm = TRUE)),
Rango = as_hms(max(tiempo_oficial, na.rm = TRUE) - min(tiempo_oficial, na.rm = TRUE)),
Varianza = round(var(tiempo_oficial, na.rm = TRUE), 2),
Desviación_Estandar = round(sd(tiempo_oficial, na.rm = TRUE), 2),
Coeficiente_Variacion = round((sd(tiempo_oficial, na.rm = TRUE) / mean(tiempo_oficial, na.rm = TRUE)) * 100, 2),
Asimetría = round(skewness(tiempo_oficial, na.rm = TRUE), 2),
Curtosis = round(kurtosis(tiempo_oficial, na.rm = TRUE), 2)
)
datatable(
tiempo_disp_forma,
options = list(dom = 't'),
rownames = FALSE
)El rango de 4 horas, 56 minutos y 55 segundos indica una amplia variabilidad en los tiempos oficiales de los corredores. La desviación estándar de 3898.73 (1 hora y 5 minutos aproximadamente) sugiere que los tiempos suelen oscilar alrededor de ±1 h de la media, lo que implica bastante variabilidad. El coeficiente de variación del 23.77% indica que la variabilidad relativa de los tiempos oficiales es considerable.
La asimetría positiva (0.15) indica que la distribución de los tiempos oficiales está ligeramente sesgada hacia la derecha, lo que significa que hay más corredores con tiempos superiores a la media. La curtosis (-0.87) indica que la distribución de los tiempos oficiales es más plana que una distribución normal, lo que sugiere que hay menos valores extremos en los tiempos oficiales de los corredores.
Como conclusión, al deberse a un conjunto de datos de una maratón de libre participación en Tokyo, podemos observar una gran variabilidad de tiempos que se distribuye de manera creciente. Es decir, en general contra mayor es el tiempo final, más gente podemos encontrar en ese nivel de carrera.
rm(tiempo_disp_forma, tiempo_stats)5.5 Tiempos Parciales
En cuanto a los tiempos parciales se analiza cada parcial de cada 5km por separado. Primero se presentan los estadísticos de centralización para cada parcial.
# cols_tiempo pero quitando timepo_oficial
tiempos_parciales <- c(
"parcial_5km","parcial_10km","parcial_15km","parcial_20km","medio_maraton",
"parcial_25km","parcial_30km","parcial_35km","parcial_40km"
)
# Tabla resumen para cada parcial
parciales_stats <- lapply(tiempos_parciales, function(parcial) {
resultadosTokyo2025 %>%
summarise(
Parcial = parcial,
Total_valores = sum(!is.na(.data[[parcial]])),
Valores_faltantes = sum(is.na(.data[[parcial]])),
Media = round(mean(.data[[parcial]], na.rm = TRUE), 2),
Mediana = median(.data[[parcial]], na.rm = TRUE),
Moda = moda(.data[[parcial]])
) %>%
mutate(
Media = as_hms(Media),
Mediana = as_hms(Mediana),
Moda = as_hms(Moda)
)
})
datatable(
do.call(rbind, parciales_stats),
options = list(dom = 't'),
rownames = FALSE,
caption = "Tabla: Estadísticos de centralización para cada parcial."
)Para representar de forma gráfica la distribución de los tiempos parciales, se ha optado por un gráfico de densidad para cada parcial, con una línea discontinua que indica la mediana de cada distribución.
df_plot <- resultadosTokyo2025 %>%
select(all_of(tiempos_parciales)) %>%
pivot_longer(everything(), names_to = "parcial", values_to = "segundos") %>%
filter(!is.na(segundos)) %>%
mutate(parcial = factor(parcial, levels = tiempos_parciales))
medianas <- df_plot %>%
group_by(parcial) %>%
summarise(mediana = median(segundos, na.rm = TRUE), .groups = "drop")
etiquetas <- setNames(
c("Parcial 5km","Parcial 10km","Parcial 15km","Parcial 20km","Medio maratón",
"Parcial 25km","Parcial 30km","Parcial 35km","Parcial 40km"),
tiempos_parciales
)
ggplot(df_plot, aes(x = segundos)) +
geom_density(fill = "#90CAF9", alpha = 0.6, na.rm = TRUE, linewidth = 0.4) +
geom_vline(data = medianas, aes(xintercept = mediana),
color = "#37474F", linetype = "dashed", linewidth = 0.6) +
facet_wrap(~parcial, scales = "free_x", ncol = 3, labeller = labeller(parcial = etiquetas)) +
scale_x_continuous(
breaks = function(x) pretty(x, n = 3), # pocas marcas por facet
labels = function(x) format(as_hms(x), "%H:%M"), # mostrar hh:mm
expand = expansion(mult = c(0.03, 0.03))
) +
scale_y_continuous(expand = expansion(mult = c(0, 0.05))) +
labs(
title = "Densidad de tiempos por parcial",
x = "Tiempo (hh:mm:ss)",
y = "Densidad"
) +
theme_minimal(base_size = 11) +
theme(
strip.text = element_text(face = "bold"),
panel.spacing = unit(1, "lines"),
panel.border = element_rect(color = "gray80", fill = NA, linewidth = 0.5),
strip.background = element_rect(fill = "#ECEFF1", color = NA),
axis.title.x = element_text(margin = margin(t = 6)),
axis.text.x = element_text(angle = 45, hjust = 1, vjust = 1), # rota etiquetas
plot.title = element_text(face = "bold")
)Se puede observar que a medida que avanza la carrera, la distribución de los tiempos parciales tiende a desplazarse hacia la derecha, indicando que los corredores tardan más en completar cada segmento a medida que avanza la maratón. Además, las distribuciones parecen volverse más anchas en los parciales posteriores, lo que sugiere una mayor variabilidad en los tiempos a medida que los corredores avanzan en la carrera y se fatigan.
En cuanto a las principales medidas de dispersión en cada parcial, se presentan a continuación:
# Tabla resumen para cada parcial
parciales_disp_forma <- lapply(tiempos_parciales, function(parcial) {
resultadosTokyo2025 %>%
summarise(
Parcial = parcial,
Minimo = as_hms(min(.data[[parcial]], na.rm = TRUE)),
Maximo = as_hms(max(.data[[parcial]], na.rm = TRUE)),
Rango = as_hms(max(.data[[parcial]], na.rm = TRUE) - min(.data[[parcial]], na.rm = TRUE)),
Varianza = round(var(.data[[parcial]], na.rm = TRUE), 2),
Desviación_Estandar = round(sd(.data[[parcial]], na.rm = TRUE), 2),
Coeficiente_Variacion = round((sd(.data[[parcial]], na.rm = TRUE) / mean(.data[[parcial]], na.rm = TRUE)) * 100, 2),
Asimetría = round(skewness(.data[[parcial]], na.rm = TRUE), 2),
Curtosis = round(kurtosis(.data[[parcial]], na.rm = TRUE), 2)
)
})
datatable(
do.call(rbind, parciales_disp_forma),
options = list(dom = 't'),
rownames = FALSE,
caption = "Tabla: Estadísticos de dispersión y forma para cada parcial."
)De forma gráfica cada boxplot representa un parcial de la maratón.
# Boxplots para cada parcial
ggplot(df_plot, aes(y = segundos, x = parcial)) +
geom_boxplot(fill = "#90CAF9", color = "gray30", outlier.color = "red", outlier.size = 1.5) +
scale_x_discrete(
labels = etiquetas
) +
scale_y_continuous(
labels = function(x) format(as_hms(x), "%H:%M"),
breaks = seq(0, 21600, by = 1800)
) +
labs(
title = "Boxplots de tiempos por parcial",
x = "Parcial",
y = "Tiempo (hh:mm:ss)"
) +
theme_minimal(base_size = 11) +
theme(
axis.text.x = element_text(angle = 45, hjust = 1),
plot.title = element_text(face = "bold")
)Estos boxplots muestran la distribución de los tiempos parciales para cada segmento de la maratón. Se observa que a medida que avanza la carrera, la variabilidad en los tiempos parece aumentar en los parciales posteriores, lo que sugiere que algunos corredores logran mantener un ritmo más constante mientras que otros experimentan una mayor desaceleración.
5.6 Ritmos
En el caso de los ritmos parciales, el análisis es practicamente idéntico al de los tiempos parciales, ya que son variables derivadas directamente de estos. Por lo tanto, se presentan directamente los boxplots para cada ritmo parcial.
# cols_ritmo
ritmos_parciales <- c(
"ritmo_5km","ritmo_10km","ritmo_15km","ritmo_20km",
"ritmo_25km","ritmo_30km","ritmo_35km","ritmo_40km"
)
# Boxplots para cada ritmo parcial
df_ritmos <- resultadosTokyo2025 %>%
select(all_of(ritmos_parciales)) %>%
pivot_longer(everything(), names_to = "ritmo", values_to = "segundos") %>%
filter(!is.na(segundos)) %>%
mutate(ritmo = factor(ritmo, levels = ritmos_parciales))
etiquetas_ritmo <- setNames(
c("Ritmo 5km","Ritmo 10km","Ritmo 15km","Ritmo 20km",
"Ritmo 25km","Ritmo 30km","Ritmo 35km","Ritmo 40km"),
ritmos_parciales
)
ggplot(df_ritmos, aes(y = segundos, x = ritmo)) +
geom_boxplot(fill = "#90CAF9", color = "gray30", outlier.color = "red", outlier.size = 1.5) +
scale_x_discrete(
labels = etiquetas_ritmo
) +
scale_y_continuous(
labels = function(x) format(as_hms(x), "%M:%S"),
breaks = seq(0, 900, by = 60)
) +
labs(
title = "Boxplots de ritmos por parcial",
x = "Parcial",
y = "Ritmo (mm:ss por km)"
) +
theme_minimal(base_size = 11) +
theme(
axis.text.x = element_text(angle = 45, hjust = 1),
plot.title = element_text(face = "bold")
)Se puede observar que los ritmos parciales tienden a aumentar (es decir, los corredores corren más lentamente) a medida que avanza la maratón. Además existen mas valores atípicos en los ritmos de los parciales posteriores, lo que indica que algunos corredores tienen dificultades significativas para mantener su ritmo a medida que avanza la carrera.
Y en cuanto al ritmo oficial.
# Grafica de densidad ritmo oficial
ggplot(resultadosTokyo2025, aes(x = ritmo_oficial)) +
geom_density(fill = "#90CAF9", alpha = 0.6, na.rm = TRUE, linewidth = 0.4) +
scale_x_continuous(
breaks = seq(60, 900, by = 60),
labels = function(x) format(as_hms(x), "%M:%S"),
expand = expansion(mult = c(0.03, 0.03))
) +
labs(
title = "Densidad del Ritmo Oficial",
x = "Ritmo Oficial (mm:ss por km)",
y = "Densidad"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold")
)Se puede observar que la mayoría de los corredores tienen un ritmo oficial entre 5 y 8 minutos por kilómetro, con un pico alrededor de los 5 minutos y 40 segundos por kilómetro. La distribución del ritmo oficial es ligeramente asimétrica hacia la derecha, lo que indica que hay algunos corredores con ritmos significativamente más lentos que elevan la media.
rm(df_plot, df_ritmos, medianas, p_density, parciales_disp_forma, parciales_stats, ritmos_parciales, tiempos_parciales)6 Análisis Bivariante y Multivariante
Una vez realizado en análisis de las variables por separado, se procede a observar cómo se relacionan unas con otras y qué información aporta dicha relación.
6.1 Tiempo oficial y género
En el siguiente gráfico se puede ver la relación entre los tiempo finales de los participantes y el genero de los mismos.
# Obtener niveles en el orden actual
niveles_genero <- unique(resultadosTokyo2025$Genero)
# Generar paleta viridis con el mismo número de niveles
paleta_base <- viridis(length(niveles_genero), option = "viridis")
names(paleta_base) <- niveles_genero
# Intercambiar colores entre Women y Non-Binary
temp_color <- paleta_base["Women"]
paleta_base["Women"] <- paleta_base["Non-binary"]
paleta_base["Non-binary"] <- temp_color
# Gráfico
ggplot(resultadosTokyo2025, aes(x = Genero, y = tiempo_oficial, fill = Genero)) +
geom_boxplot(outlier.color = "red", outlier.size = 1.5) +
scale_fill_manual(values = paleta_base) +
scale_y_continuous(
labels = function(x) as_hms(x),
breaks = seq(0, 21600, by = 1800)
) +
labs(
title = "Tiempo Oficial por Género",
x = "Género",
y = "Tiempo Oficial (hh:mm:ss)"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 16),
axis.text.x = element_text(angle = 45, hjust = 1),
legend.position = "none"
)rm(niveles_genero, paleta_base, temp_color)Se observa como los hombres tienden a tener tiempos oficiales más bajos que las mujeres y los corredores no binarios. Hay que tener en cuenta que para los corredores no binarios hay muy pocos datos, por lo que la interpretación de su boxplot puede no ser representativa.
De igual forma se puede observar a través de un gráfico de densidad que muestra la distribución de los tiempos oficiales según el género de los corredores.
# Obtener niveles de género presentes en los datos
niveles_genero <- unique(resultadosTokyo2025$Genero)
# Generar paleta viridis según esos niveles
paleta_base <- viridis(length(niveles_genero), option = "viridis")
names(paleta_base) <- niveles_genero
# Intercambiar colores entre Women y Non-Binary
temp_color <- paleta_base["Women"]
paleta_base["Women"] <- paleta_base["Non-binary"]
paleta_base["Non-binary"] <- temp_color
# Gráfico de densidad
ggplot(resultadosTokyo2025, aes(x = tiempo_oficial, fill = Genero)) +
geom_density(alpha = 0.6, na.rm = TRUE, linewidth = 0.4) +
scale_fill_manual(values = paleta_base) +
scale_x_continuous(
breaks = seq(0, 21600, by = 1800),
labels = function(x) as_hms(x),
expand = expansion(mult = c(0.03, 0.03))
) +
labs(
title = "Densidad del Tiempo Oficial por Género",
x = "Tiempo Oficial (hh:mm:ss)",
y = "Densidad"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 16),
legend.position = "top",
legend.title = element_blank()
)rm(niveles_genero, paleta_base, temp_color)Se puede observar como los hombres tienden a tener una distribución centrada en tiempos más bajos, mientras que las mujeres tienen una distribución más amplia y desplazada hacia tiempos más altos. Los corredores no binarios muestran una distribución diferente, muy estable a lo largo de los tiempos oficiales, pero debido a la baja cantidad de datos, es difícil sacar conclusiones definitivas.
6.2 Tiempo oficial y edad
A continuación vamos a analizar la relación existente entre la edad y los tiempos oficiales. Para ello, vamos a transformar la edad a categórica con el objetivo de visualizar la relación a través de grupos de edad.
Visualizamos la distribución de tiempos para cada grupo de edad a través de un box-plot:
# Crear grupos de edad de 5 años desde 15 hasta 90
resultadosTokyo2025 <- resultadosTokyo2025 %>%
mutate( tiempo_min = as.numeric(as_hms(tiempo_oficial)) / 60,
grupo_edad = cut( Edad, breaks = seq(15, 90, by = 5), right = FALSE, labels = paste(seq(15, 85, by = 5), seq(19, 89, by = 5), sep = "-") ) )
# Boxplot por grupo de edad
ggplot(resultadosTokyo2025, aes(x = grupo_edad, y = tiempo_min)) +
geom_boxplot(fill = "lightblue") +
labs(
x = "Grupo de edad",
y = "Tiempo (minutos)",
title = "Distribución del tiempo oficial por grupos de edad"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 16),
axis.text.x = element_text(angle = 45, hjust = 1),
legend.position = "none"
)Si nos fijamos en las distribuciones de tiempos para cada grupo de edad, podemos observar que tiene una forma de “U” con el primer extremo más bajo que el segundo, es decir, para rangos de edad de entre 30 y 50, tanto en media como en general, se producen mejores tiempos oficiales. También se observa que la variabilidad es similar en todos los grupos de edad salvo en el grupo “20-24” (es mayor) y “15-19”, “70-74”, “75-79” y “80-84”, (es menor).
Se podría concluir que, con los datos referentes al maratón de Tokyo de 2025, el pico de rendimiento de los deportistas se alcanza entre los 30 y 50 años y que, a partir de aquí, se produce una bajada de rendimiento visible en los tiempos finales y su distribución.
6.3 Tiempo oficial y nacionalidad
En cuanto a la nacionalidad, vamos a ver los tiempos oficiales medios por país para los países con al menos 5 corredores en el siguiente mapa.
tiempos_medios_paises <- resultadosTokyo2025 %>%
group_by(Pais_Estandarizado, CODE) %>%
summarise(
corredores = n(),
tiempo_medio = as.numeric(mean(tiempo_oficial, na.rm = TRUE)), # segundos numéricos
.groups = "drop"
) %>%
filter(corredores >= 5) %>%
mutate(
tiempo_medio_label = format(as_hms(tiempo_medio), "%H:%M:%S"),
hover_text = paste0(
"<b>", Pais_Estandarizado, "</b><br>",
"Tiempo medio oficial: ", tiempo_medio_label, "<br>",
"Número de corredores: ", corredores
)
)
tickvals <- pretty(tiempos_medios_paises$tiempo_medio, n = 5)
ticktext <- format(as_hms(tickvals), "%H:%M:%S")
# MAPA
plot_ly(
tiempos_medios_paises,
type = 'choropleth',
locations = ~CODE, # debe ser ISO-3 u otro código que use tu dataset
z = ~tiempo_medio, # valor numérico en segundos
text = ~hover_text, # HTML preformateado
hovertemplate = "%{text}<extra></extra>", # usamos el texto tal cual
colorscale = "Viridis",
reversescale = TRUE,
marker = list(line = list(color = 'white', width = 0.5))
) %>%
colorbar(title = "Tiempo medio (hh:mm:ss)", tickvals = tickvals, ticktext = ticktext) %>%
layout(
title = "Tiempo Oficial Medio por País (Maratón de Tokio)",
geo = list(showframe = FALSE, showcoastlines = FALSE, projection = list(type = 'mercator'))
)Se puede observar como en Japón, país anfitrión, el tiempo medio oficial es de aproximadamente 4 horas y 46 minutos pero cuenta con hasta casi 19000 corredores. Por otro lado, otros países con menos corredores como Kenia o Etiopía tienen tiempos medios oficiales significativamente mejores, alrededor de 2 horas y 12 minutos, lo que refleja un mayor nivel de los corredores de estos países. Por ello al analizar estos datos hay que tener en cuenta el número de corredores de cada país para interpretar correctamente los tiempos medios oficiales.
rm(tickvals, ticktext, etiquetas_ritmo, etiquetas, tiempos_medios_paises)6.4 Género y edad
Ahora vamos a relacionar la edad con el genero de los corredores para ver si existe alguna diferencia en la edad de los participantes según su género.
# Obtener los niveles de la variable Género
niveles_genero <- unique(resultadosTokyo2025$Genero)
# Crear la paleta viridis según el número de géneros
paleta_base <- viridis(length(niveles_genero), option = "viridis")
names(paleta_base) <- niveles_genero
# Intercambiar colores entre Women y Non-Binary (manteniendo Men igual)
temp_color <- paleta_base["Women"]
paleta_base["Women"] <- paleta_base["Non-binary"]
paleta_base["Non-binary"] <- temp_color
# Boxplot Edad vs Género
ggplot(resultadosTokyo2025, aes(x = Genero, y = Edad, fill = Genero)) +
geom_boxplot(outlier.color = "red", outlier.size = 1.5) +
scale_fill_manual(values = paleta_base) +
labs(
title = "Edad de los Corredores por Género",
x = "Género",
y = "Edad (años)"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 16),
axis.text.x = element_text(angle = 45, hjust = 1),
legend.position = "none"
) rm(paleta_base, temp_color, niveles_genero)Se observa que la mediana de edad es similar entre hombres y mujeres, aunque los hombres parecen tener una mayor variabilidad en sus edades. Los corredores no binarios tienen una mediana de edad más baja, pero nuevamente, el número de datos es muy limitado para hacer una interpretación sólida.
# ---- Función auxiliar para formatear segundos a "HH:MM" ----
sec_to_hhmm <- function(x) {
# x en segundos; usamos POSIXct con origin para formatear a HH:MM
format(as.POSIXct(x, origin = "1970-01-01", tz = "UTC"), "%H:%M:%S")
}
p_smooth <- ggplot(resultadosTokyo2025, aes(x = Edad, y = tiempo_oficial, color = Genero)) +
geom_point(data = resultadosTokyo2025 %>% sample_n(3000), aes(alpha = 0.3), size = 0.8, show.legend = FALSE) + # muestra una muestra
geom_smooth(method = "loess", se = TRUE, linewidth = 1) +
scale_y_continuous(labels = sec_to_hhmm) +
labs(title = "Tendencia Edad vs Tiempo (LOESS) — muestra + linea suavizada") +
theme_minimal()
p_smoothrm(p_smooth, sec_to_hhmm)6.5 Definición de estrategias
Al conocer los tiempos de paso cada 5 kilómetros se han podido obtener los ritmos en min/km de cada parcial. Siguiendo la evolución de estos ritmos parciales se pueden definir varios tipos de estrategias:
- Positiva: a medida que avanza la carrera el ritmo aumenta
- Negativa: a medida que avanza la carrera el ritmo disminuye
- Uniforme: el ritmo es igual durante toda la carrera
- Parabólica: los primeros y últimos kilómetros son más rápidos que los centrales
tolerancia <- 8 # margen en segundos
clasificar_estrategia <- function(df) {
# Seleccionar SOLO las columnas ritmo_* pero excluyendo ritmo_oficial
parciales <- df[grep("^ritmo_", names(df), value = TRUE)]
parciales <- parciales[!grepl("ritmo_oficial", names(parciales))]
# Convertir a numérico (segundos)
parciales <- as.numeric(parciales)
parciales <- parciales[!is.na(parciales)]
if (length(parciales) < 3) return(NA_character_)
# Calcular diferencias y tendencias
delta_total <- last(parciales) - first(parciales)
diferencias <- diff(parciales)
# Detectar patrón parabólico
mitad <- ceiling(length(parciales) / 2)
ritmo_inicio <- mean(parciales[1:2], na.rm = TRUE)
ritmo_medio <- parciales[mitad]
ritmo_final <- mean(tail(parciales, 2), na.rm = TRUE)
es_parabolico <- isTRUE(
(ritmo_medio - ritmo_inicio > tolerancia) &
(ritmo_medio - ritmo_final > tolerancia)
)
# Clasificación
if (isTRUE(es_parabolico)) {
estrategia <- "Parabólica"
} else if (all(abs(diferencias) < tolerancia, na.rm = TRUE)) {
estrategia <- "Uniforme"
} else if (!is.na(delta_total) && delta_total < -tolerancia) {
estrategia <- "Negativa" # termina más rápido
} else if (!is.na(delta_total) && delta_total > tolerancia) {
estrategia <- "Positiva" # termina más lento
} else {
estrategia <- "Variable"
}
return(estrategia)
}
# Aplicar al dataset
resultadosTokyo2025 <- resultadosTokyo2025 %>%
rowwise() %>%
mutate(estrategia = clasificar_estrategia(pick(everything()))) %>%
ungroup()Nota La estrategia variable no es ninguna estrategia en concreto, hace referencia a los corredores que adaptan su ritmo en cada parcial según sus sensaciones del momento, aumentándolo o disminuyéndolo en función de la fatiga percibida.
6.6 Edad y estrategia
# Calcular conteos por grupo de edad y estrategia
estrategias_edad <- resultadosTokyo2025 %>%
group_by(grupo_edad, estrategia) %>%
summarise(n = n(), .groups = "drop")
# Gráfico con colores aptos para daltónicos (Okabe–Ito)
ggplot(estrategias_edad, aes(x = grupo_edad, y = n, fill = estrategia)) +
geom_col(position = "dodge") +
scale_fill_manual(
values = c(
"#E69F00", # naranja
"#56B4E9", # azul claro
"#009E73", # verde
"#F0E442", # amarillo
"#0072B2", # azul oscuro
"#D55E00", # rojo anaranjado
"#CC79A7" # rosado
)
) +
labs(
title = "Distribución de estrategias por grupo de edad",
x = "Grupo de edad",
y = "Número de corredores",
fill = "Estrategia"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 15),
axis.text.x = element_text(angle = 45, hjust = 1),
legend.position = "bottom"
)A simple vista, puede comprobarse que independientemente del grupo de edad, la mayoría de corredores siguieron una estrategia positiva, es decir, su ritmo fue disminuyendo a lo largo de la carrera, terminando los últimos kilómetros a un ritmo más lento que los primeros.
Las tácticas de carrera negativa y parabólica fueron las menos empleadas para todos los grupos de edad. En cuanto a la estrategia uniforme, los grupos de edad en los que más corredores consiguieron mantener unos ritmos similares en todos los parciales fueron los comprendidos entre 40 y 44 y entre 45 y 49.
6.7 Género y estrategia
ggplot(resultadosTokyo2025, aes(x = Genero, fill = estrategia)) +
geom_bar(position = "fill") +
labs(
title = "Proporción de estrategias por género",
x = "Género",
y = "Proporción dentro del género",
fill = "Estrategia"
) +
scale_y_continuous(labels = scales::percent) +
scale_fill_manual(
values = c(
"#E69F00", # naranja
"#56B4E9", # azul claro
"#009E73", # verde
"#F0E442", # amarillo
"#0072B2", # azul oscuro
"#D55E00", # rojo anaranjado
"#CC79A7" # rosado
)
) +
theme_minimal(base_size = 14) +
theme(
plot.title = element_text(face = "bold", size = 16),
axis.text.x = element_text(face = "bold"),
legend.position = "bottom"
)Se puede observar que las proporciones de cada estrategia en los corredores masculinos es prácticamente igual en las corredoras femeninas.
6.8 Nivel del atleta y estrategia
colores_okabe_ito <- c(
"#E69F00", # naranja
"#56B4E9", # azul claro
"#009E73", # verde
"#F0E442", # amarillo
"#0072B2", # azul oscuro
"#D55E00", # rojo anaranjado
"#CC79A7" # rosado
)
# --- HOMBRES ---
ggplot(resultadosTokyo2025 %>% filter(Genero == "Men"),
aes(x = categoria, fill = estrategia)) +
geom_bar(position = "fill") + # proporciones
scale_y_continuous(labels = scales::percent) +
scale_fill_manual(values = colores_okabe_ito) +
labs(
title = "Estrategias de carrera según nivel (Hombres)",
x = "Nivel del atleta",
y = "Proporción",
fill = "Estrategia"
) +
theme_minimal(base_size = 14) +
theme(
axis.text.x = element_text(angle = 45, hjust = 1),
plot.title = element_text(face = "bold", size = 16),
legend.position = "bottom"
)# --- MUJERES ---
ggplot(resultadosTokyo2025 %>% filter(Genero == "Women"),
aes(x = categoria, fill = estrategia)) +
geom_bar(position = "fill") +
scale_y_continuous(labels = scales::percent) +
scale_fill_manual(values = colores_okabe_ito) +
labs(
title = "Estrategias de carrera según nivel (Mujeres)",
x = "Nivel del atleta",
y = "Proporción",
fill = "Estrategia"
) +
theme_minimal(base_size = 14) +
theme(
axis.text.x = element_text(angle = 45, hjust = 1),
plot.title = element_text(face = "bold", size = 16),
legend.position = "bottom"
)rm(estrategias_edad, colores_okabe_ito, cols_tiempo, tolerancia, moda)Estas dos gráficas ilustran la principal diferencia entre el nivel de los atletas más allá del resultado objetivo marcado por el tiempo oficial. La proporción de atletas que optaron por una táctica uniforme fue de aproximadamente un 50% en hombres y cerca de un 60% en mujeres. En los atletas de alto nivel, la proporción fue de alrededor de un 40% en hombres y un 50% en mujeres. Tanto para el sexo masculino como para el femenino, la proporción de atletas muy entrenados que llevaron una táctica uniforme está entre el 20% y el 25%. La proporción más alta de atletas que optaron por una estrategia negativa (recorrer los últimos kilómetros más rápido que los primeros) se ha dado en atletas masculinos y femeninos de nivel “moderamente entrenado”.
6.9 Cálculo del Tokyo Performance Index (TPI)
En la actual sección vamos a calcular el Tokyo Performance Index (TPI), un sistema de puntuación diseñado para evaluar el rendimiento individual de los corredores en la Maratón de Tokio 2025 mediante una medida relativa a los mejores registros conocidos.
Este índice se construye a partir del tiempo oficial final de cada participante y de un tiempo de referencia, en este caso el récord mundial vigente.
La fórmula utilizada tiene carácter exponencial, de forma que la puntuación máxima (1000 puntos) se asigna al corredor cuyo tiempo coincide con el de referencia, y el resto de participantes obtiene valores decrecientes en función de su desviación relativa.
Concretamente, el índice se define como:
\[ TPI = 1000 \times e^{-k \cdot \frac{T - T_{ref}}{T_{ref}}} \]
donde:
- ( T ) representa el tiempo oficial del corredor (en segundos),
- ( T_{ref} ) el tiempo de referencia (récord o mejor marca), y
- ( k ) un parámetro de ajuste que controla la penalización de los tiempos más lentos.
De este modo, el TPI permite normalizar los resultados y comparar rendimientos entre corredores de distinto sexo y edad, eliminando el efecto del tiempo absoluto y centrándose en la proximidad al rendimiento máximo posible.
# Récords mundiales de maratón
# Hombre: 2:00:35 (Kelvin Kiptum)
# Mujer: 2:09:56 (Ruth Chepngetich)
record_men <- 2*3600 + 0*60 + 35 # en segundos: 2h 00m 35s
record_women <- 2*3600 + 9*60 + 56 # en segundos: 2h 09m 56s
# Parámetro de penalización
k <- 5
# Calcular TPI en el data frame
resultadosTokyo2025 <- resultadosTokyo2025 %>%
mutate(
# Asignar el récord correspondiente según género
ref_seg = case_when(
Genero == "Men" ~ record_men,
Genero == "Women" ~ record_women,
TRUE ~ (record_men + record_women) / 2 # para otros géneros, usar promedio como referencia
),
# Cálculo del TPI
TPI = 1000 * exp(-k * ((tiempo_oficial - ref_seg) / ref_seg))
)
resultadosTokyo2025$ref_seg <- NULL6.9.1 Ránking de países por TPI
A partir del TPI calculado para cada atleta, vamos a calcular la media de puntos obtenida por país, para evaluar el rendimiento de cada país.
ranking_paises <- resultadosTokyo2025 %>%
group_by(Pais_Estandarizado) %>%
summarise(Media_TPI = round(mean(TPI, na.rm = TRUE), digits=2)) %>%
arrange(desc(Media_TPI)) %>%
mutate(Ranking = row_number()) %>%
select(Ranking, Pais_Estandarizado, Media_TPI)
datatable(
ranking_paises,
rownames = FALSE,
options = list(
pageLength = 5, # Mostrar 5 filas por página
dom = 'tip', # Mostrar tabla + información de paginación
ordering = TRUE,
columnDefs = list(list(className = 'dt-center', targets = c(0, 2))) # Centrar Ranking y Media_TPI
)
)ggplot(head(ranking_paises, 5), aes(x = reorder(Pais_Estandarizado, Media_TPI), y = Media_TPI, fill = Pais_Estandarizado)) +
geom_col(show.legend = FALSE, width = 0.6) +
coord_flip() +
scale_fill_viridis_d(option = "viridis") +
geom_text(aes(label = round(Media_TPI, 1)), hjust = -0.2, size = 4) +
labs(
title = "🏅 Top 5 Países por Media del Tokyo Performance Index (TPI)",
x = "País",
y = "Media del TPI"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold", size = 16),
axis.title.y = element_text(face = "bold"),
axis.title.x = element_text(face = "bold"),
axis.text = element_text(size = 12),
panel.grid.minor = element_blank(),
panel.grid.major.y = element_blank()
) +
ylim(0, max(ranking_paises$Media_TPI) * 1.1) # deja espacio para las etiquetas6.9.2 Mejor país agrupado por TPI y por categoría del corredor
Una vez realizado el ránking por países, ahora vamos a realizar el mismo proceso pero agrupando por nivel del corredor, para ver el nivel de cada país adaptado al tipo de participantes que llevó a la Maratón.
ranking_categorias <- resultadosTokyo2025 %>%
group_by(categoria, Pais_Estandarizado) %>%
summarise(Media_TPI = round(mean(TPI, na.rm = TRUE), 2), .groups = "drop") %>%
arrange(categoria, desc(Media_TPI))
top5_por_categoria <- ranking_categorias %>%
group_by(categoria) %>%
slice_max(order_by = Media_TPI, n = 5, with_ties = FALSE) %>%
mutate(Ranking = row_number()) %>%
ungroup() %>%
select(categoria, Ranking, Pais_Estandarizado, Media_TPI)
categorias <- unique(top5_por_categoria$categoria)
for (cat in categorias) {
cat("### 🏁 Categoría:", cat, "\n\n")
top5_por_categoria %>%
filter(categoria == cat) %>%
select(-categoria) %>%
kable(
caption = paste("Top 5 Países —", cat),
align = "c",
col.names = c("Ranking", "País", "Media TPI")
) %>%
print()
cat("\n\n")
}### 🏁 Categoría: Élite
Table: Top 5 Países — Élite
| Ranking | País | Media TPI |
|:-------:|:--------:|:---------:|
| 1 | Sweden | 800.49 |
| 2 | Ethiopia | 770.80 |
| 3 | Kenya | 748.09 |
| 4 | Uganda | 744.20 |
| 5 | Bahrain | 675.79 |
### 🏁 Categoría: Alto nivel
Table: Top 5 Países — Alto nivel
| Ranking | País | Media TPI |
|:-------:|:-----------:|:---------:|
| 1 | Ethiopia | 488.38 |
| 2 | France | 396.32 |
| 3 | Philippines | 342.41 |
| 4 | Taiwan | 336.20 |
| 5 | Estonia | 324.62 |
### 🏁 Categoría: Muy entrenado
Table: Top 5 Países — Muy entrenado
| Ranking | País | Media TPI |
|:-------:|:-----------:|:---------:|
| 1 | Venezuela | 287.45 |
| 2 | Uzbekistan | 276.15 |
| 3 | Switzerland | 255.58 |
| 4 | Honduras | 197.10 |
| 5 | Nepal | 179.26 |
### 🏁 Categoría: Moderadamente entrenado
Table: Top 5 Países — Moderadamente entrenado
| Ranking | País | Media TPI |
|:-------:|:------------:|:---------:|
| 1 | Bermuda | 74.54 |
| 2 | Nicaragua | 66.53 |
| 3 | Korea, North | 66.42 |
| 4 | Bulgaria | 64.16 |
| 5 | El Salvador | 63.32 |
### 🏁 Categoría: Principiante
Table: Top 5 Países — Principiante
| Ranking | País | Media TPI |
|:-------:|:---------:|:---------:|
| 1 | Tanzania | 22.19 |
| 2 | Palestine | 13.22 |
| 3 | Egypt | 13.08 |
| 4 | Brunei | 13.06 |
| 5 | Bahrain | 11.69 |
### 🏁 Categoría: No élite
Table: Top 5 Países — No élite
| Ranking | País | Media TPI |
|:-------:|:--------------:|:---------:|
| 1 | United States | 45.39 |
| 2 | Japan | 21.54 |
| 3 | Mexico | 17.52 |
| 4 | United Kingdom | 16.02 |
| 5 | China | 6.37 |
6.9.3 Podium de los mejores atletas del Maratón Tokyo 2025.
top3_corredores <- resultadosTokyo2025 %>%
arrange(desc(TPI)) %>%
slice_head(n = 3) %>%
mutate(
Puesto = row_number(),
tiempo_oficial = as_hms(tiempo_oficial) # conversión a formato hh:mm:ss
) %>%
select(Puesto, Nombre, Pais_Estandarizado, tiempo_oficial, Genero)
kable(top3_corredores)| Puesto | Nombre | Pais_Estandarizado | tiempo_oficial | Genero |
|---|---|---|---|---|
| 1 | TADESE TAKELE | Ethiopia | 02:03:23 | Men |
| 2 | DERESA GELETA | Ethiopia | 02:03:51 | Men |
| 3 | VINCENT KIPKEMOI NGETICH | Kenya | 02:04:00 | Men |
7 Identificación de Patrones y Formulación de Preguntas
Un patrón llamativo que se ha estudiado ha sido el comportamiento del tiempo oficial de los participantes. Su distribución era asimétrica y desplazada hacia la derecha, lo que quiere decir que había muy pocos corredores con marcas de élite y muchos corredores con marcas más cercanas a la media. Si se estudiaran otras “Major Marathon” se podría comparar la distribución de los tiempos en cada una de ellas y considerar la viabilidad y utilidad de establecer perfiles de maratón. Por otro lado, sería posible comparar la distribución de una “Major Marathon” con otros formatos de competición como Campeonatos Continentales, Mundiales, Juegos Olímpicos o incluso maratones enteramente populares. Dichas comparaciones podrían ser de utilidad para entrenadores y/o managers deportivos.
Otro hecho llamativo es el desplazamiento hacia la derecha de la distribución de los tiempos parciales. Algunas preguntas que pueden surgir para análisis futuros son:
¿En qué momento o momentos hay un cambio notable en el ritmo de los atletas?
¿Hay algún cambio que sea común a todos los atletas independientemente del nivel? Esto puede indicar irregularidad del terreno, cambio en los elementos ambientales, mayor o menor presencia de público, entre otros factores externos.
¿Qué factores psicofisiológicos están detrás de los cambios de ritmo? Esto permitiría plantear las planificaciones de entrenamiento teniendo en cuenta el punto crítico en que el organismo se encuentra en condiciones especialmente hostiles.
8 Conclusiones
Con el análisis univariante se han podido extraer datos referentes a la participación:
En la “Major Marathon” de Tokyo de 2025 participaron 36.173 deportistas, de los cuales 26.701 fueron hombres, 9.425 fueron mujeres y 38 deportistas se identificaron como de género no binario. Respectivamente representan el 73’83%, el 26’06% y el 0’11% del total de participantes. El menor de todos los corredores fue de 18 años y el mayor de 84, estando el común de edad comprendido entre los 39 y 50 años.
Al tener acceso a la nacionalidad de los deportistas, se ha podido ver que la mayoría de atletas provenían de Japón, país anfitrión de la maratón, seguido de Estados Unidos, Taiwán, China e Inglaterra. Los países que menos representación tuvieron se sitúan en Oriente Medio y África. Es de especial mención que no hubo ningún corredor de África Occidental y Central.
Otros datos no relacionados con la participación que se han estudiado en el análisis univariante reflejan el rendimiento de los corredores. Son el tiempo oficial, los tiempos de paso por los parciales cada 5km y la media maratón y los ritmos en min/km extraídos del recién mencionado tiempo de paso.
Observando la distribución de los tiempos de los corredores, se determina que los tiempos más comunes están entre 03:30:00 y 05:00:00, siendo pocos los atletas con una marca inferior a las 3 horas y, aún menos, a las 2 horas y 30 minutos. Esta es una distribución típica de una “Major Marathon”, ya que, aunque se reserva un hueco específico para atletas de élite (facilitando la obtención de las marcas mínimas para los campeonatos internaciones), es un formato de carrera abierta al público, por lo que la mayoría de la participación es de categoría popular.
La distribución de los tiempos parciales informa de la evolución de la carrera. La mayoría de corredores disminuyen el ritmo durante la prueba, fruto de la fatiga. En cuanto a la organización, muchos de ellos corren en grupos que se dispersan a medida que pasan los kilómetros. Es especialmente notable en los que son de categoría popular por la forma en la que está organizada la línea de salida: En ella se organizan cajones de salida definidos por las marcas de los atletas, colocándose más cerca de la línea los más rápidos. Detrás de todos los cajones (“élite”, “alto nivel”, “cajón rápido 1”, “cajón rápido 2” y demás separaciones que la organización considere) comienza la salida de los corredores populares. Es por esto que a pesar de las diferencias de forma física, en los primeros kilómetros de la maratón hay grupos grandes que van progresivamente disminuyendo.
El conocimiento del comportamiento de las variables por separado ha permitido el estudio de las relaciones entre dichas variables en el análisis bivariante.
De la relación entre el género y el tiempo oficial se ha observado una tendencia a tiempos inferiores en el sexo masculino. Se puede explicar por la superioridad físcia del sexo masculino a nivel general, aunque puede haber matices que se comentarán más adelante en el estudio de las estrategias de carrera. En cuanto a los atletas no binarios, sus datos son tan escasos que la interpretación de las gráficas puede no ser representativa.
De la relación de la edad con el tiempo oficial se ha comprobado que los grupos de edad que tienen mejores marcas están comprendidas entre los 30 y los 44 años. Son también estas edades las que tienen mayor variabilidad. Recordando lo expuesto sobre la edad en el análisis univariante, es lógico pensar que haya mayor variabilidad en edades intermedias por tener mayor participación. Los grupos de edad extremos, además de tener menos corredores, corresponden a etapas vitales delicadas (crecimiento y vejez) con unas características comunes a pesar de la diferencia que pueda haber entre individuos.
Otra observación interesante es que, al contrario que en otras pruebas del atletismo donde el pico de rendimiento se tiene a una edad más joven, en la maratón han sido atletas de más de 30 e incluso 40 años los que han conseguido mejores registros, reflejo de la tendencia del organismo a “hacerse resistente”.
De la relación entre la nacionalidad y el tiempo oficial no se pueden extraer conclusiones sólidas debida a la enorme diferencia de participación entre naciones.
A partir de los ritmos se han podido determinar estrategias para relacionarlas con otras variables ya mencionadas. La que puede ser de más impacto es la relación entre el tipo de estrategia y el nivel de los atletas:
La táctica de carrera más utilizada ha sido la positiva, aquella en la que el ritmo es cada vez más lento. Uno de las observaciones más llamativas es que más 1/4 de las corredoras y aproximadamente la mitad de los corredores de élite la utilizaban, existiendo literatura científica que defiende que la mejor opción para una prueba de estas características es el mantenimiento del ritmo. Cabe remarcar que la diferencia en la estrategia positiva entre atletas de élite o alto nivel y las demas categorías es que cuanto más bajo es el nivel, mayor es la diferencia de ritmo entre parciales. Otro detalle a destacar es que el porcentaje de mujeres de élite que consigue no variar el ritmo entre los parciales es considerablemente mayor que el porcentaje de hombres de élite.
Esta última podría abrir las puertas a una futura investigación en la que se estudien comportamientos parecidos en otras maratones. Si coinciden con los resultados del presente análisis, se procuraría conocer las causas psicológicas y fisiológicas por las que el sexo femenino, a pesar de tener marcas inferiores que el masculino a nivel global, son capaces de ser más estables durante 42km y 195m.
Otras investigaciones correspondientes a otros campos como la psicología o la sociología pueden considerar relevante la diferencia de participación entre mujeres y hombres o entre países africanos y del resto de continentes.